package org.eraser;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * oauth2.0 客户端配置
 */
@AutoConfigureAfter({Oauth2CoreConfig.class})
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    // HttpSessionOAuth2AuthorizationRequestRepository 在请求之间持久化授权请求
    @Autowired
    private AuthorizationRequestRepository<OAuth2AuthorizationRequest> requestAuthorizationRequestRepository;

    // DefaultAuthorizationCodeTokenResponseClient
    @Autowired
    private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;

    @Autowired
    private OAuth2AuthorizationRequestResolver authorizationRequestResolver;

    // HttpSessionOAuth2AuthorizedClientRepository
    @Autowired
    private OAuth2AuthorizedClientRepository authorizedClientRepository;

    // InMemoryOAuth2AuthorizedClientService
    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    // securityContext 存入 session
    @Autowired
    private SecurityContextRepository securityContextRepository;

    // 认证成功处理器 SimpleUrlAuthenticationSuccessHandler
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    // 认证成功处理器 SimpleUrlAuthenticationSuccessHandler
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * authorizationCodeGrant方法 是用于配置 OAuth 2.0 授权码授权的方法, 每项配置最终都会返回 AuthorizationCodeGrantConfigurer
     * <p>
     * AuthorizationRequestRepository 用于在请求之间持久化 OAuth2AuthorizationRequest
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CsrfTokenRepository
                // DeferredCsrfToken 延迟加载
                // CsrfToken
                // CsrfTokenRequestHandler
                .csrf((csrf) -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler()))


                // .cors(Customizer.withDefaults())

                // 所有请求都需要认证 登录页面除外
                .authorizeHttpRequests(authorize -> authorize.requestMatchers(new AntPathRequestMatcher(Oauth2CoreConfig.loginPageUri)).anonymous().anyRequest().authenticated())

                .oauth2Login(oauth2 -> oauth2
                                // todo 登陆界面由认证中心来提供
                                .loginPage(Oauth2CoreConfig.loginPageUri)
                                .authorizationEndpoint(authorization -> authorization
                                        .authorizationRequestRepository(requestAuthorizationRequestRepository)
                                        .authorizationRequestResolver(authorizationRequestResolver)
                                )

                                .clientRegistrationRepository(clientRegistrationRepository)
                                .authorizedClientRepository(authorizedClientRepository)
                                // .authorizedClientService(authorizedClientService)

                                // 重定向uri ClientRegistration.redirectUri 的值要与redirectionBaseUri匹配
                                .redirectionEndpoint(redirection -> redirection
                                        .baseUri(Oauth2CoreConfig.redirectionBaseUri)
                                )

                                .tokenEndpoint(token -> token
                                        .accessTokenResponseClient(accessTokenResponseClient)
                                )

                                // 登录成功之后跳转
                                .successHandler(authenticationSuccessHandler)

                                // 认证失败之后跳转
                                .failureHandler(authenticationFailureHandler)

                        // TODO: 2023/12/6
                        // .userInfoEndpoint(userInfo -> userInfo
                        //        .userAuthoritiesMapper(this.userAuthoritiesMapper())
                        //        .userService(this.oauth2UserService())
                        //        .oidcUserService(this.oidcUserService())
                        // )
                )

                // SecurityContext 存入 Session
                .securityContext(securityContext -> securityContext.securityContextRepository(securityContextRepository))

                // Session 并发控制
                .sessionManagement(session -> session.maximumSessions(1));

        return http.build();
    }

    /**
     * OAuth2AuthorizedClientManager
     * <pre>
     * 用于管理已授权的客户端 还用于对客户端进行授权(或重新授权)
     * 有两个重要的实现 :
     * DefaultOAuth2AuthorizedClientManager
     * AuthorizedClientServiceOAuth2AuthorizedClientManager
     * </pre>
     * 区别
     * <pre>
     * DefaultOAuth2AuthorizedClientManager:
     * 旨在 HttpServletRequest 上下文中使用
     * AuthorizedClientServiceOAuth2AuthorizedClientManager:
     * 旨在 HttpServletRequest 上下文之外使用
     * </pre>
     * 这两个类还使用不同的接口来持久化 "已授权客户端对象"
     * <pre>
     * 前者 OAuth2AuthorizedClientRepository
     * 后者 OAuth2AuthorizedClientService
     * </pre>
     * OAuth2AuthorizedClientRepository
     * OAuth2AuthorizedClientService
     * <pre>
     * 两者一样用于管理已授权客户端,将"访问令牌凭证"与"客户端"和"资源所有者"关联起来
     * OAuth2AuthorizedClientRepository 旨在 HttpServletRequest 上下文中使用
     * 当在 HttpServletRequest 上下文之外运行时, 应使用 OAuth2AuthorizedClientService
     * </pre>
     */
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager() {
        /*

        // 客户端模式的Provider 客户端凭据授权类型通常用于服务器到服务器的通信
        // OAuth2AuthorizedClientProvider clientCredentials = OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build();

        // 一个授权码类型的Provider
        // OAuth2AuthorizedClientProviderBuilder.builder().authorizationCode().build();

        OAuth2AuthorizedClientProvider authorizationCode = OAuth2AuthorizedClientProviderBuilder.builder()
                // 授权码授权类型 AuthorizationCodeOAuth2AuthorizedClientProvider
                .authorizationCode()
                // 支持令牌刷新授权类型 RefreshTokenOAuth2AuthorizedClientProvider
                .refreshToken()
                // 客户端凭据授权类型
                .clientCredentials()
                // 创建
                .build();

        AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizationCode);

        // 设置一个函数 (函数是 Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper)
        // 函数本身是返回一个上下文属性映射器 OAuth2AuthorizeRequest 和 "属性" 的映射。 (存储在OAuth2AuthorizationContext中)。 这些属性可以在授权过程中使用, 例如在发起授权请求时添加自定义参数。
        authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper());
        return authorizedClientManager;

        */


        // OAuth2AuthorizedClientManager 负责与一个或多个 OAuth2AuthorizedClientProvider 合作，管理 OAuth2AuthorizedClient 的授权（或重新授权）
        // OAuth2AuthorizedClientManager 首次登录认证并不参与
        OAuth2AuthorizedClientProvider authorizationCode = OAuth2AuthorizedClientProviderBuilder.builder()
                /*
                 * 授权码模式 AuthorizationCodeOAuth2AuthorizedClientProvider 用于授权(或重新授权)。
                 * 要关注 OAuth2AuthorizationRequestRedirectFilter(负责将授权请求重定向到认证服务器获取授权码), OAuth2LoginAuthenticationFilter(负责拿着授权码向认证服务器认证) 这俩过滤器。
                 * OAuth2LoginAuthenticationFilter >>> getAuthenticationManager() >>> ProviderManager >>> OAuth2LoginAuthenticationProvider >>> OAuth2AuthorizationCodeAuthenticationProvider >>> DefaultAuthorizationCodeTokenResponseClient
                 *
                 * OIDC 获取用户信息也是在上述流程中完成的 :
                 * OAuth2LoginAuthenticationProvider >>> DefaultOAuth2UserService >>> OAuth2LoginAuthenticationToken >>> OAuth2AuthenticationToken >>> AbstractAuthenticationProcessingFilter >>> sessionStrategy (创建并存入session) >>> successfulAuthentication (SecurityContext 往 ThreadLocal 中存一份)
                 */
                .authorizationCode()

                /*
                 * 支持令牌刷新授权类型 RefreshTokenOAuth2AuthorizedClientProvider。
                 * RefreshTokenGrantBuilder 里有个 OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> 发送RefreshToken请求的。
                 * 你可以使用 refreshToken(Consumer<RefreshTokenGrantBuilder> builderConsumer) 来自定义 OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient
                 * 默认 RefreshTokenOAuth2AuthorizedClientProvider 内部会使用 DefaultRefreshTokenTokenResponseClient()
                 */
                .refreshToken()
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizationCode);
        return authorizedClientManager;
    }


    /**
     * contextAttributesMapper()
     * <pre>
     * 这个函数常用于 PasswordOAuth2AuthorizedClientProvider 密码模式
     * 该函数将用户名和密码从 OAuth2AuthorizeRequest 映射到授权上下文的属性中
     * 这些属性将在发起授权请求时使用
     * </pre>
     * OAuth2AuthorizedClientProvider
     * <pre>
     * 执行 authorize() 方法时
     * 可以从 OAuth2AuthorizationContext 中访问这些属性
     * </pre>
     * OAuth2AuthorizeRequest 类的意义
     * <pre>
     * OAuth2AuthorizedClientManager 将使用 OAuth2AuthorizeRequest 发起授权请求
     * 发起请求并返回 “已授权客户端”
     *
     * {@code
     * OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
     * .withClientRegistrationId(clientRegistrationId)
     * .principal(principal)
     * .build();
     *
     * authorizedClientManager.authorize(authorizeRequest);}
     * </pre>
     */
    public Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
        return authorizeRequest -> {
            Map<String, Object> contextAttributes = Collections.emptyMap();

            // authorizeRequest 是 OAuth2AuthorizeRequest 类
            HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
            assert servletRequest != null;
            String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
            String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);

            if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                contextAttributes = new HashMap<>();

                // `PasswordOAuth2AuthorizedClientProvider` 需要这两个属性
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            }
            return contextAttributes;
        };
    }


}
